package domain

import (
	"encoding/json"
	"errors"
	"fmt"
	"gitee.com/sunny-zhangqiang/zcache/comm"
	"gitee.com/sunny-zhangqiang/zcache/comm/slru"
	"gopkg.in/yaml.v3"
	"os"
	"strings"
	"sync"
	"time"
)

const (
	defaultWildCardDepth  = 3
	defaultDomainLists    = 4
	defaultSlruMaxEntries = 256
)

var (
	ErrNotFound = errors.New("not found")
)

type Domains struct {
	dir     string
	metaMux sync.RWMutex
	meta    map[string]int64
	lists   [defaultDomainLists]*slru.S4LRU
}

var domains *Domains

func updateMetaInfo(dir string) error {
	data, err := os.ReadFile(dir + ".meta")
	if err != nil {
		return err
	}

	meta := make(map[string]int64, 128)
	err = json.Unmarshal(data, meta)
	if err != nil {
		return err
	}

	domains.metaMux.Lock()
	domains.meta = meta
	domains.metaMux.Unlock()
	return nil
}

func Init(dir string, updateTimeSec int) error {
	domains = &Domains{
		dir: dir,
	}

	for i := 0; i < defaultDomainLists; i++ {
		domains.lists[i] = slru.New(defaultSlruMaxEntries, 0, nil)
	}

	err := updateMetaInfo(dir)
	if err != nil {
		return fmt.Errorf("load meta info failed, err: %v", err)
	}

	if updateTimeSec == 0 {
		updateTimeSec = 15
	}

	go func() {
		ticker := time.NewTicker(time.Second * time.Duration(updateTimeSec))
		defer ticker.Stop()
		for {
			<-ticker.C
			updateMetaInfo(dir)
		}
	}()

	return nil
}

func getHostMeta(host string) int64 {
	domains.metaMux.RLock()
	defer domains.metaMux.RUnlock()
	version, ok := domains.meta[host]
	if !ok {
		return -1
	}
	return version
}

func loadDomainConf(host string, idx uint64) (*Domain, error) {
	path := domains.dir + host + ".conf"
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read %s conf failed, err: %v", path, err)
	}

	conf := &Domain{}
	err = yaml.Unmarshal(data, conf)
	if err != nil {
		return nil, fmt.Errorf("decode %s conf failed, err: %v", path, err)
	}

	err = CheckAndFixDomainConf(conf)
	if err != nil {
		return nil, fmt.Errorf("check and fix %s conf failed, err: %v", path, err)
	}

	domains.lists[idx].Set(host, conf)
	return conf, nil
}

func lookupDomain(host string) (*Domain, error) {
	version := getHostMeta(host)
	if version == -1 {
		return nil, ErrNotFound
	}

	hash := comm.FnvHash(comm.String2Bytes(host))
	idx := hash % defaultDomainLists
	v, ok := domains.lists[idx].Get(host)
	if !ok {
		return loadDomainConf(host, idx)
	}

	domain := v.(*Domain)
	if domain.Version != version {
		return loadDomainConf(host, idx)
	}

	return domain, nil
}

func buildSubHost(ss []string, idx int) string {
	sb := &strings.Builder{}
	sb.WriteString("*.")
	for i := idx; i < len(ss); i++ {
		sb.WriteString(ss[i])
		if i != len(ss)-1 {
			sb.WriteByte('.')
		}
	}
	return sb.String()
}

func LookupDomainConf(host string) (*Domain, error) {
	ret, err := lookupDomain(host)
	if err == nil {
		return ret, nil
	}

	if err != ErrNotFound {
		return nil, err
	}

	sl := strings.Split(host, ".")
	for i := 1; i < len(sl); i++ {
		if i > defaultWildCardDepth {
			return nil, ErrNotFound
		}
		subHost := buildSubHost(sl, i)
		ret, err = lookupDomain(subHost)
		if err == nil {
			return ret, nil
		}
		if err != ErrNotFound {
			return nil, err
		}
	}

	return nil, ErrNotFound
}

func LookupDomainPathConf(host string, path string) (*PathConfig, error) {
	domainConf, err := LookupDomainConf(host)
	if err != nil {
		return nil, err
	}

	for _, v := range domainConf.Path {
		if v.regex.MatchString(path) {
			return v, nil
		}
	}

	return domainConf.PathConfig, nil
}
